package org.xiaoxin.onecoupon.engine.service.impl;

import cn.hutool.bloomfilter.BloomFilter;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.xiaoxin.onecoupon.engine.common.constant.EngineRedisConstant;
import org.xiaoxin.onecoupon.engine.common.context.UserContext;
import org.xiaoxin.onecoupon.engine.common.enums.CouponTemplateStatusEnum;
import org.xiaoxin.onecoupon.engine.dao.entity.CouponTemplateDO;
import org.xiaoxin.onecoupon.engine.dao.mapper.CouponTemplateMapper;
import org.xiaoxin.onecoupon.engine.dto.req.CouponTemplateQueryReqDTO;
import org.xiaoxin.onecoupon.engine.dto.resp.CouponTemplatePageQueryRespDTO;
import org.xiaoxin.onecoupon.engine.dto.resp.CouponTemplateQueryRespDTO;
import org.xiaoxin.onecoupon.engine.service.CouponTemplateService;
import org.xiaoxin.onecoupon.framework.exception.ClientException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author xiaoxin
 * 2025/4/22
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplateDO> implements CouponTemplateService {
   private  final StringRedisTemplate stringRedisTemplate;
   private  final RedissonClient redissonClient;
   private  final  CouponTemplateMapper couponTemplateMapper;

   private final RBloomFilter<String> couponTemplateQueryBloomFilter;
   @Override
   public CouponTemplateQueryRespDTO findCouponTemplate(CouponTemplateQueryReqDTO requestParam) {
      //查询Redis中是否有缓存信息
      String couponTemplateCacheKey = String.format(EngineRedisConstant.COUPON_TEMPLATE_KEY, requestParam.getCouponTemplateId());
      Map<Object, Object> couponTemplateCacheMap = stringRedisTemplate.opsForHash().entries(couponTemplateCacheKey);
      // 如果存在直接返回，不存在需要通过双重判定锁的形式读取数据库中的记录
      if (MapUtil.isEmpty(couponTemplateCacheMap)){
         //判断布隆过滤器中是否存在指定模板id，不存在直接返回错误，这是第一道防线
         if(!couponTemplateQueryBloomFilter.contains(requestParam.getCouponTemplateId())){
            throw  new ClientException("优惠劵模板不存在");
         }
         //第二道防线，空值缓存，因为第一道防线布隆过滤器可能误判
         String couponTemplateIsNullKey = String.format(EngineRedisConstant.COUPON_TEMPLATE_IS_NULL_KEY, requestParam.getCouponTemplateId());
         if (stringRedisTemplate.hasKey(couponTemplateIsNullKey)) {
            throw  new ClientException("优惠劵模板不存在");
         }
         // 获取优惠券模板分布式锁
         RLock lock = redissonClient.getLock(String.format(EngineRedisConstant.LOCK_COUPON_TEMPLATE_KEY, requestParam.getCouponTemplateId())) ;
         lock.lock();
         try {
            //双重判定空值缓存是否存在
            if (stringRedisTemplate.hasKey(couponTemplateIsNullKey)) {
               throw  new ClientException("优惠劵模板不存在");
            }
            // 通过双重判定锁优化大量请求无意义查询数据库
            couponTemplateCacheMap=stringRedisTemplate.opsForHash().entries(couponTemplateCacheKey);
            if (MapUtil.isEmpty(couponTemplateCacheMap)){
               LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                       .eq(CouponTemplateDO::getId, requestParam.getCouponTemplateId())
                       .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                       .eq(CouponTemplateDO::getStatus, CouponTemplateStatusEnum.ACTIVE.getStatus());
               CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
               //如果没有查到设置控制缓存
               if (couponTemplateDO == null){
                  stringRedisTemplate.opsForValue().set(couponTemplateIsNullKey,"",30, TimeUnit.MINUTES);
                  throw  new ClientException("优惠劵已过期或者不存在");
               }
               //通过将数据库的记录序列化成 JSON 字符串放入 Redis 缓存
               CouponTemplatePageQueryRespDTO actualRespDTO = BeanUtil.copyProperties(couponTemplateDO, CouponTemplatePageQueryRespDTO.class);
               Map<String, Object> cacheTargetMap = BeanUtil.beanToMap(actualRespDTO, false, true);
               Map<String, String> actualCacheTargetMap = cacheTargetMap.entrySet().stream()
                       .collect(Collectors.toMap(
                               Map.Entry::getKey,
                               entry -> entry.getValue() != null ? entry.getValue().toString() : ""
                       ));

               // 通过 LUA 脚本执行设置 Hash 数据以及设置过期时间
               String luaScript = "redis.call('HMSET', KEYS[1], unpack(ARGV, 1, #ARGV - 1)) " +
                       "redis.call('EXPIREAT', KEYS[1], ARGV[#ARGV])";
               List<String> keys = Collections.singletonList(couponTemplateCacheKey);
               List<String> args = new ArrayList<>(actualCacheTargetMap.size() * 2 + 1);
               actualCacheTargetMap.forEach((key, value) -> {
                  args.add(key);
                  args.add(value);
               });

               // 优惠券活动过期时间转换为秒级别的 Unix 时间戳
               args.add(String.valueOf(couponTemplateDO.getValidEndTime().getTime() / 1000));

               // 执行 LUA 脚本
               stringRedisTemplate.execute(
                       new DefaultRedisScript<>(luaScript, Long.class),
                       keys,
                       args.toArray()
               );
               couponTemplateCacheMap = cacheTargetMap.entrySet()
                       .stream()
                       .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

            }
         }finally {
            lock.unlock();
         }
      }
      return  BeanUtil.mapToBean(couponTemplateCacheMap,CouponTemplateQueryRespDTO.class,false, CopyOptions.create());
   }
}
